import Module from './module'
import { assert, forEachValue } from '../util'

/**
 * 类的属性列表：
 * 1）root：保存模块集合的根模块
 */
export default class ModuleCollection {
  // rawRootModule参数为Store的选项对象
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    this.register([], rawRootModule, false)
  }

  /**
   * 该方法总是从根模块开始，根据path中指定的路径依次向下查找，获得path数组的最后一个成员
   * 对应的模块
   * @param path 数组类型，查询的路径
   */
  get (path) {
    return path.reduce((module, key) => {
      return module.getChild(key)
    }, this.root)
  }

  // 该方法根据path指定的路径，生成目标模块的命名空间的字符串
  // 具体过程是：从根模块开始，至path中指定的最后一个模块为止，
  // 所有经历的模块中，如果模块的namespaced属性为true，则会
  // 被添加到namespace中，实例：
  /**
   * let storeObj = {
   *    modules:{
   *      a:{
   *        namespaced: true,
   *        modules:{
   *          b:{
   *            modules:{
   *              c:{
   *                  namespaced: true
   *              }
   *            }
   *          }
   *        }
   *      }
   *    }
   * };
   * 生成c模块的命名空间时，最终的结果为'a/c/'，注意，因为b不为命名空间模块，所以不在最终结果中。
   */
  getNamespace (path) {
    let module = this.root
    return path.reduce((namespace, key) => {
      module = module.getChild(key)
      return namespace + (module.namespaced ? key + '/' : '')
    }, '')
  }

  update (rawRootModule) {
    update([], this.root, rawRootModule)
  }

  /**
   * 例子：
   * let storeConfig = {
   *    modules: {
   *      first: {
   *        modules: {
   *          second: {
   *          }
   *        }
   *      }
   *    }
   * }
   *
   * 该方法用于解析所有的模块及其子模块，根模块保存在当前对象的root属性上，子模块保存在父模块的
   * _children属性中，详情可查看module.js。
   *
   * 所以上方的例子会解析为：
   * this.root = {
   *    _children:{
   *      first:{
   *        _children:{
   *          second:{}
   *        }
   *      }
   *    }
   * };
   * 注意，这里的对象都是Module对象，这里只是为了简单表达模块的层次结构。
   *
   * @param path 当前注册模块的路径，从根模块开始到当前模块为止的
   *      所有模块，如上例所示，如果在解析second对应的模块时，则path为['first', 'second']。
   * @param rawModule 当前正在被注册的模块的原始选项对象
   * @param runtime 是否是通过registerModule方法动态注册的模块，true表示是通过registerModule方法
   *    动态注册的。
   */
  register (path, rawModule, runtime = true) {
    if (process.env.NODE_ENV !== 'production') {
      assertRawModule(path, rawModule)
    }

    const newModule = new Module(rawModule, runtime)

    //如果path，说明正在解析根模块
    if (path.length === 0) {
      this.root = newModule
    } else {
      // path.slice(0, -1)的含义是获得当前注册模块之前（不包括当前模块）的模块名组成的数组，也就是所有的祖先模块。
      // parent为当前注册模块的根模块
      // Todo 但是感觉这种实现方式不好，这里的get方法就是在获得当前模块的父模块，但是父模块可以通过register方法直接传入，而不需要在这里查找。
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // register nested modules
    if (rawModule.modules) {
      // key是rawModule.modules对象的键
      // rawChildModule是rawModule.modules对象中键对应的值，也就是rawModule模块的子模块
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        // Todo 像上方Todo所说，可以在此处通过this.register(...,newModule)的方式，在最后再加一个newModule，这样不需要调用get查找了。
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }

  unregister (path) {
    const parent = this.get(path.slice(0, -1))
    const key = path[path.length - 1]
    if (!parent.getChild(key).runtime) return

    parent.removeChild(key)
  }
}

function update (path, targetModule, newModule) {
  if (process.env.NODE_ENV !== 'production') {
    assertRawModule(path, newModule)
  }

  // update target module
  targetModule.update(newModule)

  // update nested modules
  if (newModule.modules) {
    for (const key in newModule.modules) {
      if (!targetModule.getChild(key)) {
        if (process.env.NODE_ENV !== 'production') {
          console.warn(
            `[vuex] trying to add a new module '${key}' on hot reloading, ` +
            'manual reload is needed'
          )
        }
        return
      }
      update(
        path.concat(key),
        targetModule.getChild(key),
        newModule.modules[key]
      )
    }
  }
}

const functionAssert = {
  assert: value => typeof value === 'function',
  expected: 'function'
}

const objectAssert = {
  assert: value => typeof value === 'function' ||
    (typeof value === 'object' && typeof value.handler === 'function'),
  expected: 'function or object with "handler" function'
}

// getters和mutations必须为函数，而actions可以为函数或者对象
const assertTypes = {
  getters: functionAssert,
  mutations: functionAssert,
  actions: objectAssert
}

// 该方法主要用于判断getters、mutations和actions对象中的键对应的值是否为期望的类型，
// 如果不是，则显示错误提示。
function assertRawModule (path, rawModule) {
  Object.keys(assertTypes).forEach(key => {
    if (!rawModule[key]) return

    const assertOptions = assertTypes[key]

    // rawModule[key]是一个对象，forEachValue函数会遍历该对象中的
    // 键值对，forEachValue的回调函数中，type是上述对象的键，而value是键对应的值
    forEachValue(rawModule[key], (value, type) => {
      assert(
        assertOptions.assert(value),
        makeAssertionMessage(path, key, type, value, assertOptions.expected)
      )
    })
  })
}

function makeAssertionMessage (path, key, type, value, expected) {
  let buf = `${key} should be ${expected} but "${key}.${type}"`
  if (path.length > 0) {
    buf += ` in module "${path.join('.')}"`
  }
  buf += ` is ${JSON.stringify(value)}.`
  return buf
}
